Skip to content

feat(account): super-admin read-only "view as" (customer impersonation)#262

Merged
kilbot merged 16 commits into
mainfrom
worktree-super-admin-view-as
Jul 6, 2026
Merged

feat(account): super-admin read-only "view as" (customer impersonation)#262
kilbot merged 16 commits into
mainfrom
worktree-super-admin-view-as

Conversation

@kilbot

@kilbot kilbot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Super-admin "view as" — read-only account impersonation

Lets an owner-allowlisted account browse the entire /account area as another customer, read-only, to verify exactly what downloads/entitlements that customer sees. You stay logged in as yourself; a target identity is layered on top for display + reads only.

Spec: docs/superpowers/specs/2026-07-05-super-admin-view-as-design.md · Plan: docs/superpowers/plans/2026-07-05-super-admin-view-as.md (both local scratch, gitignored).

How it works

  • Two identities per request. Your Medusa session token is always yours (getSessionCustomer()) — used for the admin gate + audit. A plain httpOnly cookie (wcpos-impersonate) names a target. When you're inside /account and your real session is an admin, getCustomer() and the order fetchers transparently resolve the target instead (via Medusa's built-in /admin reads with the existing MEDUSA_ADMIN_API_TOKEN). Every downstream entitlement/license/download path is already customer-agnostic, so the whole account renders as them. No backend change.
  • Entry: owner-only /account/admin → enter an email → rate-limited, audited lookup → redirect to /account, now viewing as them. An amber banner shows who you're inspecting with a one-click Exit.
  • Read-only: assertViewOnly() blocks every mutating account/cart route (profile edit, machine/Discord removal, cart/checkout) with a 403. Reads — including the actual download path — stay open so you can confirm a file really downloads.

Security model

Authority to impersonate is decided only from your real session's email against a config-in-code allowlist (src/lib/admin.ts) — never from the cookie. A planted/forged cookie does nothing without an admin session. Scoping to /account is enforced by a middleware-set request header that is stripped on all other paths so it can't be client-spoofed.

Independently adversarially reviewed. Core authority model + order isolation + write-blocking all verified solid. Three review findings fixed in 213d71e: (1) strip spoofable scope header in middleware, (2) OAuth callback uses getSessionCustomer(), (3) stale-target auto-exits to owner.

Validation

  • ✅ 1180 unit/integration tests pass (vitest run) — includes the security invariant truth-table, the two-part override, write-block on every guarded route, and an end-to-end resolve+orders integration test.
  • next build clean, no type errors.
  • ✅ ESLint clean.

⚠️ Owner action required before this works in production

  1. Confirm the allowlist email. src/lib/admin.ts has ADMIN_EMAILS = ['paul@kilbot.com']. This must exactly match the email on your real Medusa customer account or the feature won't activate for you. (Your session email showed as paul@kilbot.com.au — please set whichever is correct.)
  2. Verify the admin token authorizes /admin reads. The feature reuses MEDUSA_ADMIN_API_TOKEN (today only used by Discord sync) to read /admin/customers + /admin/orders. Confirm it works:
    curl -s -H "Authorization: Bearer $MEDUSA_ADMIN_API_TOKEN" "$MEDUSA_BACKEND_URL/admin/customers?limit=1&fields=id,email" → should return a customers array, not 401/403. If it 401s, we need a narrow custom admin route on wcpos-medusa (follow-up).
  3. Live smoke (couldn't be automated — needs your real session + prod Medusa): log in → /account/admin → enter a known customer's email → confirm the Downloads tab shows their entitlements, a profile edit is blocked (403), and Exit returns you to your own account.

Follow-ups (out of scope)

  • Inline useActionState error messaging on the admin page (currently the lookup miss re-renders without an inline message).
  • Audit each individual download performed while inspecting (start/stop are already audited).

Summary by CodeRabbit

  • New Features
    • Added an admin “inspect customer” page with read-only impersonation, including locale-aware start and exit.
    • Introduced an impersonation banner that shows the viewed customer and lets admins exit impersonation.
    • Enabled impersonation-aware customer order viewing (including by-id lookup).
  • Bug Fixes
    • Enforced view-only restrictions across account, cart, profile, and inspection/license endpoints (403 with read_only_inspection).
    • Hardened impersonation authorization with rate limiting, improved redirects/locales, and ensured OAuth profile sync targets the real session owner.
    • Improved request routing by sanitizing x-wcpos-account-request headers via middleware.
  • Tests
    • Expanded coverage for impersonation, view-only guards, admin authorization, and middleware header handling.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kilbot has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 685608c7-ab1a-4488-ba19-e99387241e41

📥 Commits

Reviewing files that changed from the base of the PR and between 1c33300 and ccf116e.

📒 Files selected for processing (2)
  • src/lib/discord/medusa-admin.test.ts
  • src/lib/discord/medusa-admin.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/discord/medusa-admin.test.ts

📝 Walkthrough

Walkthrough

This PR adds admin impersonation support, routes reads through the impersonated customer where applicable, blocks mutating routes during impersonation, and updates middleware, account UI, and exit/start flows to manage impersonation state.

Changes

Admin Impersonation Feature

Layer / File(s) Summary
Admin email allowlist
src/lib/admin.ts, src/lib/admin.test.ts
Adds ADMIN_EMAILS and isAdmin() with normalized, fail-closed matching.
Impersonation cookie mechanism
src/lib/impersonation.ts, src/lib/impersonation.test.ts, src/lib/impersonation.integration.test.ts
Defines the impersonation cookie, ViewOnlyError, getImpersonation(), assertViewOnly(), startImpersonation(), and stopImpersonation().
Admin customer lookup helpers
src/lib/discord/medusa-admin.ts, src/lib/discord/medusa-admin.test.ts
Adds findAdminCustomerByEmail(), getAdminCustomerById(), and getAdminCustomerOrderById().
Impersonation-aware customer/session resolution
src/lib/medusa-auth.ts, src/lib/medusa-auth.test.ts, src/app/api/auth/[provider]/callback/route.ts(.test.ts)
Adds getSessionCustomer(), reworks getCustomer(), and updates OAuth profile sync to use the signed-in session customer.
Impersonation-aware order fetching
src/lib/customer-orders.ts, src/lib/customer-orders.test.ts
Adds impersonated order helpers and routes order reads through admin-sourced target orders when impersonation is active.
Admin action and page to start impersonation
src/app/[locale]/account/admin/actions.ts(.test.ts), src/app/[locale]/account/admin/page.tsx
Adds startImpersonationAction() and the /account/admin inspection page/form.
Account layout banner and exit route
src/app/[locale]/account/layout.tsx, src/components/account/impersonation-banner.tsx, src/app/api/account/impersonate/exit/route.ts
Updates the account layout, adds ImpersonationBanner, and adds GET/POST exit handlers.
View-only guards on mutating API routes
src/app/api/account/licenses/.../route.ts, src/app/api/account/profile/route.ts, src/app/api/store/cart/*/route.ts (+tests)
Adds assertViewOnly() checks returning 403 read_only_inspection on mutating endpoints during impersonation.
Middleware header sanitization
src/middleware.ts, src/middleware.test.ts
Strips client-supplied x-wcpos-account-request and re-stamps it only for account-scoped flows.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AdminPage
  participant startImpersonationAction
  participant isAdmin
  participant findAdminCustomerByEmail
  participant startImpersonation

  AdminPage->>startImpersonationAction: submit target email
  startImpersonationAction->>isAdmin: verify real session
  startImpersonationAction->>findAdminCustomerByEmail: resolve target customer
  alt not found
    startImpersonationAction-->>AdminPage: {error: not_found}
  else found
    startImpersonationAction->>startImpersonation: set impersonation cookie
    startImpersonationAction-->>AdminPage: redirect /account
  end
Loading
sequenceDiagram
  participant Middleware
  participant getImpersonation
  participant getCustomer
  participant AdminAPI
  participant SessionAPI

  Middleware->>Middleware: strip spoofed account-request header
  Middleware->>Middleware: stamp header only for account-scoped paths
  getImpersonation->>getImpersonation: read header, cookie, and session
  getImpersonation-->>getCustomer: impersonation state
  alt impersonating
    getCustomer->>AdminAPI: getAdminCustomerById(targetId)
  else not impersonating
    getCustomer->>SessionAPI: getSessionCustomer()
  end
Loading

Possibly related PRs

  • wcpos/wcpos-com#147: Modifies the same order-fetching layer that this PR extends with impersonation-aware reads.
  • wcpos/wcpos-com#155: Overlaps on src/lib/medusa-auth.ts and the getCustomer() flow reworked here.
  • wcpos/wcpos-com#169: Touches the OAuth callback handler that this PR updates to use getSessionCustomer().
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: a read-only super-admin customer impersonation flow in account.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-super-admin-view-as

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown

🚀 Preview: https://wcpos-rn4yksq3c-wcpos.vercel.app

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (7)
src/middleware.test.ts (1)

137-150: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add regression coverage for early-return non-account routes.

This test only exercises the generic /api/* branch. Please also cover /?wc-api=am-software-api and updates.wcpos.com/api/*, since those branches can bypass the sanitized header forwarding path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/middleware.test.ts` around lines 137 - 150, The current middleware test
only covers the generic /api/* path, so add regression coverage for the other
early-return non-account routes that may bypass the sanitized header forwarding
path. Extend middleware.test.ts with cases for the root ?wc-api=am-software-api
route and updates.wcpos.com/api/*, using middleware() and the
ACCOUNT_REQUEST_HEADER assertions to verify the client-supplied header is still
stripped from the forwarded NextResponse.next({ request }) headers.
src/lib/impersonation.test.ts (1)

65-71: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing coverage for the cookie-delete-throws branch (lines 52-56 in impersonation.ts).

This test verifies cookieStore.delete is called when session isn't admin, but doesn't exercise the catch path where delete() throws (the real-world RSC scenario per Next.js docs, since .set/.delete are only supported in Server Functions/Route Handlers). Add a case where cookieStore.delete throws to confirm getImpersonation still resolves to null without propagating the error.

Suggested additional test
+  it('returns null even if clearing the cookie throws (read-only RSC context)', async () => {
+    cookieStore.value = 'cus_target'
+    headerStore.value = '1'
+    cookieStore.delete.mockImplementation(() => {
+      throw new Error('Cookies can only be modified in a Server Action or Route Handler')
+    })
+    getSessionCustomer.mockResolvedValue({ email: 'attacker@evil.com' })
+    await expect(getImpersonation()).resolves.toBeNull()
+  })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/impersonation.test.ts` around lines 65 - 71, Add test coverage for
the error-handling path in getImpersonation where cookieStore.delete throws in
the non-admin branch. Extend the existing impersonation tests to mock
cookieStore.delete throwing, then verify getImpersonation still returns null and
does not propagate the exception, using the getImpersonation function and
cookieStore delete mock to locate the behavior.
src/lib/impersonation.ts (1)

70-75: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Consider defense-in-depth: verify admin status inside startImpersonation too.

The function trusts the doc comment's precondition ("already verified the caller is an admin") without re-checking. If a future caller invokes this without that check, there's no internal guard against a non-admin session setting the cookie.

♻️ Optional defensive check
+import { getSessionCustomer } from '`@/lib/medusa-auth`'
+import { isAdmin } from '`@/lib/admin`'
+
 export async function startImpersonation(targetId: string): Promise<void> {
+  const session = await getSessionCustomer()
+  if (!isAdmin(session?.email)) throw new Error('Not authorized to impersonate')
   const cookieStore = await cookies()
   cookieStore.set(IMPERSONATION_COOKIE, targetId, IMPERSONATION_COOKIE_OPTIONS)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/impersonation.ts` around lines 70 - 75, `startImpersonation`
currently relies only on the caller’s precondition and can set
`IMPERSONATION_COOKIE` without verifying the session is an admin. Add an
internal admin check at the start of `startImpersonation` in
`src/lib/impersonation.ts` before calling `cookies()` and setting the cookie,
using the existing auth/admin validation path already used by the admin action
so the function is safe even if called directly.
src/lib/customer-orders.ts (1)

188-203: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid paging the full impersonation order list here.
fetchImpersonatedOrders() goes through listAdminCustomerOrders, which can walk up to 5,000 orders to find one match. The admin /admin/orders endpoint accepts id, so this path can use a single filtered lookup instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/customer-orders.ts` around lines 188 - 203, The getOrderById path is
unnecessarily loading the full impersonation order list through
fetchImpersonatedOrders/listAdminCustomerOrders. Update getOrderById to use a
single filtered admin orders lookup when impersonated access is active, passing
the orderId through the existing fetch/admin order retrieval flow instead of
scanning all impersonated orders. Keep the fallback behavior with getAuthToken
and fetchOrders unchanged, and use the existing getOrderById,
fetchImpersonatedOrders, and fetchOrders symbols to locate the logic.
src/app/api/account/licenses/[licenseId]/discord/members/[memberId]/route.test.ts (1)

11-15: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Guard mock added, but the 403 branch itself is untested here.

This mock only keeps existing tests passing; no test asserts DELETE returns 403 read_only_inspection when assertViewOnly rejects with ViewOnlyError (unlike src/app/api/account/profile/route.test.ts, which added an explicit case for this). Consider adding an analogous test, e.g. mocking assertViewOnly to reject once and asserting the response status/body.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/app/api/account/licenses/`[licenseId]/discord/members/[memberId]/route.test.ts
around lines 11 - 15, The current `route.test.ts` only mocks `assertViewOnly` in
`@/lib/impersonation`, but it does not cover the `DELETE` 403
`read_only_inspection` branch. Add an explicit test for the `DELETE` handler in
this route that makes `assertViewOnly` reject with `ViewOnlyError`, then assert
the response status and body match the expected 403 `read_only_inspection`
behavior, similar to the pattern used in `profile/route.test.ts`.
src/app/[locale]/account/admin/actions.test.ts (1)

1-57: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Solid coverage of forbidden/not_found/success paths.

Consider adding a case for the rate_limited branch (Line 40 in actions.ts) since it's currently untested.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`[locale]/account/admin/actions.test.ts around lines 1 - 57, Add test
coverage for the rate-limited path in startImpersonationAction: mock the rate
limiter so consume() returns a failed result, then assert the action returns the
rate_limited error and does not call findAdminCustomerByEmail or
startImpersonation. Keep the new test alongside the existing
startImpersonationAction cases in actions.test.ts and use the existing mocked
createRateLimiter/clientIp setup to target this branch.
src/app/[locale]/account/admin/page.tsx (1)

13-19: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Result of startImpersonationAction is silently discarded.

On forbidden/not_found/rate_limited, the form just re-renders with no feedback — the admin can't tell why "View as" did nothing. The code comment already flags this as a known follow-up (useActionState).

Want me to wire this page up with useActionState to surface the error to the admin now?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`[locale]/account/admin/page.tsx around lines 13 - 19, The submit
flow in the admin account page is discarding the result from
startImpersonationAction, so forbidden/not_found/rate_limited cases re-render
without any admin-visible feedback. Update the submit/use server flow in
page.tsx to use useActionState (or equivalent action state handling) so the
returned error is stored and rendered inline, and keep the existing
redirect-on-success behavior from startImpersonationAction.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/`[locale]/account/admin/actions.ts:
- Around line 32-40: The rate limit key in the admin action is being derived
from untrusted forwarded headers via clientIp() on a synthetic Request, which
allows spoofing. Update the action to use a trusted proxy-derived client address
or validate the forwarded headers before passing them to the limiter, and prefer
passing the existing Headers object into clientIp() instead of constructing a
new Request. Keep the fix localized around getSessionCustomer(), isAdmin(),
clientIp(), and limiter.consume().
- Around line 42-51: The impersonation redirect currently uses a bare account
path, which drops the active locale and can send users to the default-language
page. Update the redirect logic in the impersonation start flow (the action that
calls startImpersonation and redirect) and the impersonation exit route to
preserve or pass through locale, either by threading locale into the target path
or by using the locale-aware redirect helper already used elsewhere. Keep the
redirect destination explicitly locale-prefixed so users return to the same
locale they came from.

In
`@src/app/api/account/licenses/`[licenseId]/discord/members/[memberId]/route.ts:
- Line 13: The view-only authorization guard is duplicated across this route and
the other listed handlers, so extract the repeated try/catch logic into a shared
helper in the impersonation utilities and reuse it everywhere. Add a helper
alongside assertViewOnly and ViewOnlyError in src/lib/impersonation.ts that
performs the guard and throws the same 403 response shape, then update the route
handler here (and the matching profile, machines, cart, and payment session
routes) to call that helper instead of inlining the block.

In `@src/app/api/store/cart/complete/route.ts`:
- Line 6: The store cart completion route is not protected by the
impersonation/view-only guard because `assertViewOnly()` depends on
`x-wcpos-account-request`, which is only set by `src/middleware.ts` for account
APIs. Update the middleware scope to stamp that trusted header for the store
cart/checkout API paths as well, or add a direct impersonation check inside
`route.ts` before completing the cart; use `assertViewOnly` and the
`/api/store/cart/complete` handler as the key touchpoints.

In `@src/lib/customer-orders.ts`:
- Around line 95-106: fetchImpersonatedOrders currently lets
listAdminCustomerOrders throw, which can bubble up and crash
getOrdersPage/getAllOrders/getOrderById under impersonation. Mirror the error
handling used in fetchOrders: wrap the admin API call in a try/catch, log or
otherwise handle the failure, and return a safe fallback such as null or an
empty order result so impersonated reads degrade gracefully instead of throwing.

In `@src/lib/discord/medusa-admin.ts`:
- Around line 99-134: Align the error handling in findAdminCustomerByEmail with
getAdminCustomerById so a medusaAdminFetch failure does not escape as an
unhandled exception from startImpersonationAction callers. Add a try/catch
around the admin/customers lookup in findAdminCustomerByEmail, return null on
failure to match the lookup contract, and use the same customer-fetch symbols
(medusaAdminFetch, findAdminCustomerByEmail, getAdminCustomerById) to keep both
lookup paths consistent. Also update getAdminCustomerById’s catch to log the
failure before returning null so real admin API outages are distinguishable from
a missing customer.
- Around line 99-115: The customer lookup in findAdminCustomerByEmail currently
returns the first Medusa result, which can pick a guest record when the same
email exists for both guest and registered customers. Update this function to
prefer a customer with has_account set to true before falling back to any
remaining match, using the existing medusaAdminFetch and page.customers data to
disambiguate safely. Ensure the returned MedusaCustomer from
findAdminCustomerByEmail reflects the registered profile when duplicates exist.

In `@src/middleware.ts`:
- Around line 163-178: The account-request handling in `middleware` is too broad
because `pathnameWithoutLocale.startsWith('/account')` also matches routes like
`/accounting`. Update the path check to only cover the `/account` segment
boundary by using the same `pathnameWithoutLocale` guard in `src/middleware.ts`,
so the `NextRequest` header injection and `intlMiddleware` call only run for
`/account` and its subpaths.
- Around line 120-128: The API-route branch in middleware is dropping
impersonation context for cart endpoints that now use assertViewOnly(). Update
the NextResponse.next handling in middleware.ts so /api/store/cart/* also gets
the same request header stamping as /api/account/*, using the existing
ACCOUNT_REQUEST_HEADER with sanitizedHeaders. Keep the current account behavior
intact, but ensure the cart/checkout guard paths can still see
getImpersonation() and enforce the intended 403.

---

Nitpick comments:
In `@src/app/`[locale]/account/admin/actions.test.ts:
- Around line 1-57: Add test coverage for the rate-limited path in
startImpersonationAction: mock the rate limiter so consume() returns a failed
result, then assert the action returns the rate_limited error and does not call
findAdminCustomerByEmail or startImpersonation. Keep the new test alongside the
existing startImpersonationAction cases in actions.test.ts and use the existing
mocked createRateLimiter/clientIp setup to target this branch.

In `@src/app/`[locale]/account/admin/page.tsx:
- Around line 13-19: The submit flow in the admin account page is discarding the
result from startImpersonationAction, so forbidden/not_found/rate_limited cases
re-render without any admin-visible feedback. Update the submit/use server flow
in page.tsx to use useActionState (or equivalent action state handling) so the
returned error is stored and rendered inline, and keep the existing
redirect-on-success behavior from startImpersonationAction.

In
`@src/app/api/account/licenses/`[licenseId]/discord/members/[memberId]/route.test.ts:
- Around line 11-15: The current `route.test.ts` only mocks `assertViewOnly` in
`@/lib/impersonation`, but it does not cover the `DELETE` 403
`read_only_inspection` branch. Add an explicit test for the `DELETE` handler in
this route that makes `assertViewOnly` reject with `ViewOnlyError`, then assert
the response status and body match the expected 403 `read_only_inspection`
behavior, similar to the pattern used in `profile/route.test.ts`.

In `@src/lib/customer-orders.ts`:
- Around line 188-203: The getOrderById path is unnecessarily loading the full
impersonation order list through
fetchImpersonatedOrders/listAdminCustomerOrders. Update getOrderById to use a
single filtered admin orders lookup when impersonated access is active, passing
the orderId through the existing fetch/admin order retrieval flow instead of
scanning all impersonated orders. Keep the fallback behavior with getAuthToken
and fetchOrders unchanged, and use the existing getOrderById,
fetchImpersonatedOrders, and fetchOrders symbols to locate the logic.

In `@src/lib/impersonation.test.ts`:
- Around line 65-71: Add test coverage for the error-handling path in
getImpersonation where cookieStore.delete throws in the non-admin branch. Extend
the existing impersonation tests to mock cookieStore.delete throwing, then
verify getImpersonation still returns null and does not propagate the exception,
using the getImpersonation function and cookieStore delete mock to locate the
behavior.

In `@src/lib/impersonation.ts`:
- Around line 70-75: `startImpersonation` currently relies only on the caller’s
precondition and can set `IMPERSONATION_COOKIE` without verifying the session is
an admin. Add an internal admin check at the start of `startImpersonation` in
`src/lib/impersonation.ts` before calling `cookies()` and setting the cookie,
using the existing auth/admin validation path already used by the admin action
so the function is safe even if called directly.

In `@src/middleware.test.ts`:
- Around line 137-150: The current middleware test only covers the generic
/api/* path, so add regression coverage for the other early-return non-account
routes that may bypass the sanitized header forwarding path. Extend
middleware.test.ts with cases for the root ?wc-api=am-software-api route and
updates.wcpos.com/api/*, using middleware() and the ACCOUNT_REQUEST_HEADER
assertions to verify the client-supplied header is still stripped from the
forwarded NextResponse.next({ request }) headers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b5a4a903-1e06-4059-a6d4-5c4a450f705c

📥 Commits

Reviewing files that changed from the base of the PR and between 2ef1925 and 213d71e.

📒 Files selected for processing (35)
  • src/app/[locale]/account/admin/actions.test.ts
  • src/app/[locale]/account/admin/actions.ts
  • src/app/[locale]/account/admin/page.tsx
  • src/app/[locale]/account/layout.tsx
  • src/app/api/account/impersonate/exit/route.ts
  • src/app/api/account/licenses/[licenseId]/discord/members/[memberId]/route.test.ts
  • src/app/api/account/licenses/[licenseId]/discord/members/[memberId]/route.ts
  • src/app/api/account/licenses/[licenseId]/machines/[machineId]/route.test.ts
  • src/app/api/account/licenses/[licenseId]/machines/[machineId]/route.ts
  • src/app/api/account/profile/route.test.ts
  • src/app/api/account/profile/route.ts
  • src/app/api/auth/[provider]/callback/route.test.ts
  • src/app/api/auth/[provider]/callback/route.ts
  • src/app/api/store/cart/complete/route.test.ts
  • src/app/api/store/cart/complete/route.ts
  • src/app/api/store/cart/line-items/route.test.ts
  • src/app/api/store/cart/line-items/route.ts
  • src/app/api/store/cart/payment-sessions/route.test.ts
  • src/app/api/store/cart/payment-sessions/route.ts
  • src/app/api/store/cart/route.test.ts
  • src/app/api/store/cart/route.ts
  • src/components/account/impersonation-banner.tsx
  • src/lib/admin.test.ts
  • src/lib/admin.ts
  • src/lib/customer-orders.test.ts
  • src/lib/customer-orders.ts
  • src/lib/discord/medusa-admin.test.ts
  • src/lib/discord/medusa-admin.ts
  • src/lib/impersonation.integration.test.ts
  • src/lib/impersonation.test.ts
  • src/lib/impersonation.ts
  • src/lib/medusa-auth.test.ts
  • src/lib/medusa-auth.ts
  • src/middleware.test.ts
  • src/middleware.ts

Comment thread src/app/[locale]/account/admin/actions.ts
Comment thread src/app/[locale]/account/admin/actions.ts Outdated
Comment thread src/app/api/store/cart/complete/route.ts
Comment thread src/lib/customer-orders.ts
Comment thread src/lib/discord/medusa-admin.ts
Comment thread src/lib/discord/medusa-admin.ts
Comment thread src/middleware.ts
Comment thread src/middleware.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 213d71ee93

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/middleware.ts Outdated

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kilbot has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@wcpos-bot

wcpos-bot Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Review-fix triage for commit 7e7472c.

Thread File Issue Decision Commit
3525574253 actions.ts Rate limiter used spoofable forwarded IP headers Fixed: keyed limiter by verified admin session email 7e7472c
3525574255 actions.ts Impersonation redirects dropped locale Fixed: start and exit redirects preserve locale 7e7472c
3525574259 route.ts Cart completion guard could not see impersonation scope Fixed: middleware stamps store cart API scope 7e7472c
3525574260 customer-orders.ts Admin order list failures could crash impersonated account pages Fixed: catch/log and degrade to empty result 7e7472c
3525574263 medusa-admin.ts Duplicate email lookup could choose guest customer Fixed: prefer has_account=true customer 7e7472c
3525574264 medusa-admin.ts Admin customer lookup error handling was inconsistent Fixed: log and return null on lookup failures 7e7472c
3525574265 middleware.ts Store cart APIs missed trusted impersonation header Fixed: include /api/store/cart scope 7e7472c
3525574266 middleware.ts /accounting matched /account impersonation scope Fixed: account path segment-boundary check 7e7472c
3525574490 middleware.ts Codex duplicate: store cart APIs missed trusted impersonation header Fixed: same middleware scope update 7e7472c
CodeRabbit review body actions.test.ts Missing rate_limited branch coverage Fixed: added rate-limit test 7e7472c
CodeRabbit review body route.test.ts Missing Discord member DELETE read-only 403 test Fixed: added ViewOnlyError branch coverage 7e7472c
CodeRabbit review body impersonation.test.ts Missing cookie-delete-throws coverage Fixed: added read-only context delete failure test 7e7472c
CodeRabbit review body impersonation.ts startImpersonation trusted caller precondition only Fixed: added internal admin check 7e7472c
CodeRabbit review body customer-orders.ts getOrderById scanned full impersonated order list Fixed: added single filtered admin order lookup 7e7472c
CodeRabbit review body middleware.test.ts Missing early-return header sanitization coverage Fixed: added legacy rewrite and updates API regression tests 7e7472c

Skipped threads:

Thread Reason for skipping
3525574256 Declined as a broad helper/refactor request across multiple routes. It is not a correctness or missing-safety issue, and the PR-fix proportionality rules for this branch explicitly prohibit adding abstractions/helpers for non-correctness cleanup.
CodeRabbit review body: admin page useActionState Skipped because the PR body explicitly lists inline useActionState error messaging as a follow-up/out of scope for this PR.

Validation:

  • pnpm install --frozen-lockfile: passed
  • pnpm lint: passed with 5 existing warnings outside this change
  • pnpm exec tsc --noEmit: passed
  • pnpm test:unit: passed (1195 tests)
  • pnpm test:e2e --project=chromium: blocked by environment; Chromium cannot load libglib-2.0.so.0 in this container

Fresh post-resolution thread inventory: 0 unresolved review threads.

@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown

🚀 Preview: https://wcpos-hftap7rwu-wcpos.vercel.app

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
src/app/api/account/impersonate/exit/route.ts (2)

7-25: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider reusing next-intl's getPathname instead of hand-rolled prefixing.

The locale !== defaultLocale ? '/${locale}/account' : '/account' logic duplicates next-intl's own localePrefix: 'as-needed' behavior outside the shared routing config. If that config ever changes (e.g. to always or domain-based routing), this manual branch will silently drift. getPathname({ locale, href: '/account' }) from @/i18n/navigation encodes the same intent without duplicating the prefixing rule.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/api/account/impersonate/exit/route.ts` around lines 7 - 25, The
accountUrl helper is duplicating locale prefixing logic instead of using the
shared next-intl routing API. Update accountUrl in the impersonate exit route to
build the /account target through getPathname from `@/i18n/navigation`, using the
resolved locale and keeping the existing referer/explicitLocale selection logic,
so prefix behavior stays aligned with the central routing config.

27-32: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Same PII-in-logs concern as actions.ts.

authLogger.info\Impersonation STOP: admin=${session?.email ?? 'unknown'}`` logs the admin's email; flagged by static analysis (CWE-532). Lower risk than logging the target customer's email (this is the admin's own identity for audit purposes), but consider standardizing on a non-PII identifier if the same convention is applied elsewhere.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/api/account/impersonate/exit/route.ts` around lines 27 - 32, The exit
impersonation flow in exit() is still logging PII by using session?.email in
authLogger.info, which triggers the same CWE-532 concern as actions.ts. Update
the log statement in exit to avoid raw email addresses and standardize on a
non-PII identifier already available from getSessionCustomer() or another stable
audit-safe value, keeping the Impersonation STOP event usable without exposing
email data.

Source: Linters/SAST tools

src/lib/discord/medusa-admin.ts (1)

96-111: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Inconsistent error handling vs. sibling lookups.

findAdminCustomerByEmail and getAdminCustomerById both catch medusaAdminFetch failures and log via infraLogger.error, returning null. getAdminCustomerOrderById lets failures propagate uncaught. The only current caller (fetchImpersonatedOrderById in src/lib/customer-orders.ts) happens to wrap it in try/catch, but this breaks the just-established convention in this file and is fragile against future callers.

♻️ Suggested fix for consistency
 export async function getAdminCustomerOrderById(
   customerId: string,
   orderId: string
 ): Promise<MedusaOrder | null> {
   const query = new URLSearchParams({
     limit: '1',
     customer_id: customerId,
     id: orderId,
   })
-
-  const page = await medusaAdminFetch<AdminOrdersResponse>(
-    `/admin/orders?${query.toString()}`
-  )
-  const [order] = page.orders ?? []
-  return order?.id === orderId ? order : null
+  try {
+    const page = await medusaAdminFetch<AdminOrdersResponse>(
+      `/admin/orders?${query.toString()}`
+    )
+    const [order] = page.orders ?? []
+    return order?.id === orderId ? order : null
+  } catch (error) {
+    infraLogger.error`Failed to fetch admin order ${orderId} for customer ${customerId}: ${error}`
+    return null
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/discord/medusa-admin.ts` around lines 96 - 111,
getAdminCustomerOrderById is the only lookup here that does not mirror the
try/catch + infraLogger.error + null fallback used by findAdminCustomerByEmail
and getAdminCustomerById. Wrap the medusaAdminFetch call in
getAdminCustomerOrderById with the same error handling pattern, log the failure
through infraLogger.error with enough context to identify the order/customer
lookup, and return null on failure so the behavior stays consistent for future
callers.
src/app/[locale]/account/admin/actions.ts (1)

33-58: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Avoid logging customer email addresses in plaintext.

Static analysis flags lines 39, 51, and 55 for logging PII (CWE-532). target=${email} / target=${target.email} log the impersonated customer's email address on every lookup attempt. Consider logging target.id (or a redacted/hashed form) instead of the raw email to preserve audit value while reducing PII exposure in log aggregators.

🔒 Suggested reduction of logged PII
-    authLogger.info`Impersonation lookup miss: admin=${adminEmail} target=${email}`
+    authLogger.info`Impersonation lookup miss: admin=${adminEmail}`
     return { error: 'not_found' }
   }

-  authLogger.info`Impersonation START: admin=${adminEmail} target=${target.email} (${target.id})`
+  authLogger.info`Impersonation START: admin=${adminEmail} target_id=${target.id}`
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`[locale]/account/admin/actions.ts around lines 33 - 58, The
impersonation audit logs in startImpersonationAction are leaking customer email
PII by writing the target email in plaintext. Update the authLogger.info calls
in the lookup-miss and START paths to log a non-PII identifier such as
target.id, or a redacted/hashed value, while keeping the adminEmail context for
auditability. Keep the existing flow in startImpersonationAction and only change
the logged target fields so the messages remain useful without exposing raw
customer emails.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/app/`[locale]/account/admin/actions.ts:
- Around line 33-58: The impersonation audit logs in startImpersonationAction
are leaking customer email PII by writing the target email in plaintext. Update
the authLogger.info calls in the lookup-miss and START paths to log a non-PII
identifier such as target.id, or a redacted/hashed value, while keeping the
adminEmail context for auditability. Keep the existing flow in
startImpersonationAction and only change the logged target fields so the
messages remain useful without exposing raw customer emails.

In `@src/app/api/account/impersonate/exit/route.ts`:
- Around line 7-25: The accountUrl helper is duplicating locale prefixing logic
instead of using the shared next-intl routing API. Update accountUrl in the
impersonate exit route to build the /account target through getPathname from
`@/i18n/navigation`, using the resolved locale and keeping the existing
referer/explicitLocale selection logic, so prefix behavior stays aligned with
the central routing config.
- Around line 27-32: The exit impersonation flow in exit() is still logging PII
by using session?.email in authLogger.info, which triggers the same CWE-532
concern as actions.ts. Update the log statement in exit to avoid raw email
addresses and standardize on a non-PII identifier already available from
getSessionCustomer() or another stable audit-safe value, keeping the
Impersonation STOP event usable without exposing email data.

In `@src/lib/discord/medusa-admin.ts`:
- Around line 96-111: getAdminCustomerOrderById is the only lookup here that
does not mirror the try/catch + infraLogger.error + null fallback used by
findAdminCustomerByEmail and getAdminCustomerById. Wrap the medusaAdminFetch
call in getAdminCustomerOrderById with the same error handling pattern, log the
failure through infraLogger.error with enough context to identify the
order/customer lookup, and return null on failure so the behavior stays
consistent for future callers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: dd78a26a-0a86-41ab-9a88-6f9c83443ed1

📥 Commits

Reviewing files that changed from the base of the PR and between 213d71e and 7e7472c.

📒 Files selected for processing (14)
  • src/app/[locale]/account/admin/actions.test.ts
  • src/app/[locale]/account/admin/actions.ts
  • src/app/[locale]/account/admin/page.tsx
  • src/app/[locale]/account/layout.tsx
  • src/app/api/account/impersonate/exit/route.ts
  • src/app/api/account/licenses/[licenseId]/discord/members/[memberId]/route.test.ts
  • src/lib/customer-orders.test.ts
  • src/lib/customer-orders.ts
  • src/lib/discord/medusa-admin.test.ts
  • src/lib/discord/medusa-admin.ts
  • src/lib/impersonation.test.ts
  • src/lib/impersonation.ts
  • src/middleware.test.ts
  • src/middleware.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/lib/customer-orders.test.ts
  • src/app/[locale]/account/admin/page.tsx
  • src/lib/impersonation.ts
  • src/app/[locale]/account/layout.tsx
  • src/lib/customer-orders.ts
  • src/middleware.ts

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kilbot has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown

🚀 Preview: https://wcpos-ky7fto8hf-wcpos.vercel.app

@wcpos-bot

wcpos-bot Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Review-fix triage for commit a4a2f7d.

Thread File Issue Decision Commit
CodeRabbit review body 4631985105 actions.ts Impersonation lookup-miss and START audit logs included target customer email PII Fixed: lookup misses now log a short audit hash of the normalized target email; START logs target_id only while preserving adminEmail context a4a2f7d
CodeRabbit review body 4631985105 route.ts Exit accountUrl duplicated locale prefixing logic instead of using shared next-intl routing Fixed: accountUrl now builds /account via getPathname with the resolved locale and keeps the existing explicit-locale/referer selection a4a2f7d
CodeRabbit review body 4631985105 route.ts Impersonation STOP audit log used session email PII Fixed: STOP logs admin_id from the session customer instead of raw email a4a2f7d
CodeRabbit review body 4631985105 medusa-admin.ts getAdminCustomerOrderById lacked the sibling lookup try/catch + infraLogger.error + null fallback Fixed: wrapped the admin order fetch, logs order/customer lookup context, returns null on failure, and added unit coverage a4a2f7d

Remaining unresolved review threads after post-push GraphQL inventory: 0.

Validation:

  • pnpm install --frozen-lockfile: passed
  • pnpm lint: passed with 5 existing warnings outside this change
  • pnpm exec tsc --noEmit: passed
  • pnpm exec vitest run src/lib/discord/medusa-admin.test.ts: passed (9 tests)
  • pnpm test:unit: passed (1196 tests)
  • pnpm test:e2e --project=chromium: local container blocked by missing libglib-2.0.so.0 (exit 1)
  • Hosted GitHub checks on a4a2f7d: Test, E2E Tests, CodeQL, Deploy Preview, and CodeRabbit passed; Deploy Production skipped

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kilbot has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

🚀 Preview: https://wcpos-phij384e5-wcpos.vercel.app

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kilbot has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

🚀 Preview: https://wcpos-j8w4cn0yd-wcpos.vercel.app

@kilbot kilbot merged commit 1aa4f93 into main Jul 6, 2026
9 checks passed
@kilbot kilbot deleted the worktree-super-admin-view-as branch July 6, 2026 11:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant